Practices & Tools

Svelte 4 Under the Hood

Typing, slot-bindings, Custom Elements, and more

Simon Holthausen

The web framework Svelte is currently undergoing lots of great activity. In December 2022, after a long beta phase, version 1 of the official Svelte meta framework SvelteKit was finally released. Then, in June 2023, Svelte 4 was released. This article takes a look at the latest major version’s new features and at what else can be expected in the future.

Fast moving times

Svelte offers a full-stack solution for developing a wide variety of applications. From static websites to highly dynamic apps, it supports every use case. Following the release, the maintainer team returned its focus to Svelte itself.

Svelte 3 released more than four years ago, which is eons in JavaScript framework time. Svelte remained fresh during that time, but Node.js and browser APIs evolved steadily. Things that used to require polyfills are now supported by all browsers. At the same time, demands on web frameworks have also steadily increased as their use cases become more dynamic and complex.

Changes at a glance

So how does Svelte 4 fit into these new circumstances? First of all, Svelte 4 is mainly a maintenance release that increases the minimum version requirements for its dependencies. Node.js 16 is now the minimum version, and those who use TypeScript should use at least version 5.0. Other dependencies like the build tool (Rollup, Webpack, or Vite) must also be upgraded to a current version. There are also a number of smaller breaking changes that should only affect a few applications. A lot happened under the hood. Let’s go into detail about the individual points.

Sensible defaults

Svelte comes with a robust animation library out-of-the-box. For example, with just a few lines of code, elements can be given input or output transitions to make them fade when they disappear. Until now, these transitions were global by default. This means that no matter where they were in the component space, they were always played. But this led to unpleasant surprises, like when a complete page navigation happened and was held up by a tiny transition deep in the old page. So, transitions are now local by default. “Local” means that a transition won’t play if it’s inside a nested control flow block (#each/#if/#await/#key) and not the direct parent block, but a block above it when created/destroyed. In the following example, the input transition is only played when success changes from false to true, but not when show changes from false to true.

iJS Newsletter

Keep up with JavaScript’s latest news!

{#if show}
  ...
  {#if success}
    <p in:slide>Success</p>
  {/each}

To make transitions global, add the |global modifier. Then, they will always play.

{#if show}
  ...
  {#if success}
    <p in:slide|global>Success</p>
  {/each}
{/if}

Stricter typing

Another change is the stricter typing of various APIs. One of these is createEventDispatcher. This is used to fire events within a component for other components to listen for. createEventDispatcher also supports a type argument to specify which events are allowed and what payload they contain, but not if they’re mandatory or not. You can now specify whether an argument is optional, mandatory, or if it should never be set.

import { createEventDispatcher } from 'svelte';

const dispatch = createEventDispatcher<{
  optional: number | null;
  pflicht: string;
  nie: null;
}>();

// Svelte version 3:
dispatch('optional');
dispatch('required'); // Second argument missing - no error
dispatch('nie', 'surprise'); // Second argument set anyway - no error

// Svelte version 4 with strict TypeScript null checks:
dispatch('optional');
dispatch('required'); // Error, second argument missing
dispatch('nie', 'surprise'); // Error, second argument set

Consistent slot bindings

Svelte provides a handy feature to let the user component determine UI content at specific locations in a component. You can also pass variables “up” to this user component, which can read them with let:variableName and use them in their template. The following example illustrates the API using a generic list component.

<script>
  export let items;
</script>

<ul>
{#each items as item}
  <li><slot item={item} /></li>
{/each}
</ul>

<slot name="summary" length={items.length} />
<script>
  import List from './List.svelte';
  let todos = ['write article', 'get people excited about svelte'];
</script>

<List items={todos} let:item>
  <span>Zu tun: {item}</p>

  <p slot="summary" let:length>Insgesamt {length} Einträge</p>
</List>

As you can see, both a default slot (in this case, for rendering a list entry) can be used, and named slots (in this case for a summary) can be defined. In Svelte 3, you could previously use let:item (here a binding of the default slot) in the named slot too (in this case, in the summary slot). But in this example, this would lead to undefinable behavior, since let:item is assigned to one item from the list each. Which item should appear in the summary slot that isn’t part of the list? In Svelte 4, this gives a compilation error.

Revision of the Custom Elements mode

One of the biggest changes in Svelte has been made to the Custom Elements mode (or Web Components mode). The framework allows you to compile written Svelte components so that they can be used as Custom Elements afterwards.

<!-- Definition (Svelte 3) -->
<svelte:options tag="my-custom-element" />

<script>
  export let name
</script>

<p>My name is {name}</p>

<!-- Usage (in other file) -->
<my-custom-element name="Simon"></my-custom-element>

This mode worked well for simpler use cases, but had some limitations and pitfalls. For example, in this mode all of the Svelte components had to be defined as custom elements and used accordingly, even if only a fraction of them would actually be publicly available. The component lifecycle was also not optimal, leading to confusion and bugs.

Svelte 4 fundamentally revised the concept of the Custom Elements mode. Svelte components are now no longer compiled entirely as Custom Elements, but are only optionally transformed into one with the help of a wrapper. This frees developers from Svelte 3’s “all or nothing” solution and fixes some bugs. New configuration options have also been introduced to determine whether a property should be reflected as an attribute in the HTML and what the values should be treated as when transforming from attributes to property (and vice versa). In the following example, it’s defined that the property elementIndex is available as attribute element-index in the HTML and is of the type number.

<svelte:options customElement={{
  tag: "my-custom-element",
  props: { elementIndex: { reflect: true, type: 'Number', attribute: 'element-index' } }
}} />

<script>
  export let elementIndex;
</script>

...

The property can now be set programmatically in JavaScript by instance.elementIndex = 1, and declaratively in HTML by .

These changes are largely backwards compatible. The main change is that the inner Svelte component isn’t created until the element is also added to the DOM. Overall, the revision should make developing custom elements or web components much easier and allow for more use cases.

Migrating to the new version: The migration script

Besides the detailed breaking changes explained here, there are a few strange ones, which we won’t list. It should also be clear from the changes listed that many will barely affect any applications. Some of them can be migrated automatically. For this, the Svelte team provides a migration script that can be executed with the command line npx svelte-migrate@latest svelte-4. Iit automatically sets the required minimum versions of dependencies in the package.json and optionally offers a backwards-compatible migration of the transition change.

EVERYTHING AROUND ANGULAR

The iJS Angular track

Many changes under the hood

In addition to visible changes, a lot has also been done under the hood. For example, the repository has been adapted to modern conditions and converted into a monorepository to provide space for possible future packages. The release process has also been simplified and automated. The conversion of all code from TypeScript to JavaScript with JSDoc is also exciting. With the help of JSDoc, you can write JavaScript code that is just as typesafe as TypeScript code. The big advantage for a framework like Svelte is that this largely eliminates the need for a build script. Converting TypeScript and bundling to JavaScript is no longer necessary. This also makes it easier to “have a quick look” at the Svelte source code in the node\_modules folder, and above all it shows the original unaltered code. For Svelte maintainers, this means faster debugging. Bugs can be fixed faster through a better feedback cycle without a build step. It should also make it easier for occasional contributors to get involved in Svelte development.

Therefore, a build script is omitted and additional work was put into type generation. So that TypeScript and the editors understand which interfaces and methods Svelte makes available, d.ts files must be created. Their generation is now much better. All definitions are now packed into one large file and superfluous private type definitions are removed. This makes their size much smaller, and the IDEs’ autocomplete suggestions are much better as a result.

To save even more kilobytes, Svelte says goodbye to CommonJS code and only delivers the JavaScript code in the original ESM version. Today’s bundlers can all handle this format, so there shouldn’t be any problems. Since the code is no longer packaged, source maps can also be saved. Overall, Svelte’s npm package size can be reduced from over 10 to under 3 megabytes.

The new website is impressive

The team has also completely revised the official website. The single-page endless scrolling documentation has been split into sensible sub-pages. The new space is also used for a few documentation extensions, which now go into more detail in some places. There is much more information about how to work with TypeScript and available interfaces. Night owls will be happy about the new dark mode. The practically inclined will have fun with the new tutorial, which, in addition to Svelte itself, also highlights the metaframework SvelteKit. All of this is available in the browser, without having to set anything up.

This is just the beginning

In summary, Svelte 4 contains mostly incremental changes that affect only a few applications. For many third-party libraries, you should be able to support both Svelte 3 and 4 in parallel. The biggest changes are probably the reworked Custom Elements mode and work done under the hood. This yields a faster development flow for Svelte maintainers and better IDE intelligence for users.

Is that it? Will we wait years for the next major Svelte release? Not at all! The Svelte maintainers made it clear that this is just the first step into an exciting future. If Svelte is a garden, then version 4 pulls the weeds and prepares the ground for many new magnificent plants. For example, the team wants to fundamentally overhaul the compiler and runtime to make Svelte more scalable, predictable, and even faster. This project won’t take years. Thanks to the company Vercel, three developers now work on Svelte full-time. The maintainer team wants to give some first insights into concrete changes later this year. How exciting!

Top Articles About Practices & Tools

Sign up for the iJS newsletter and stay tuned to the latest JavaScript news!

 

BEHIND THE TRACKS OF iJS

JavaScript Practices & Tools

DevOps, Testing, Performance, Toolchain & SEO

Angular

Best-Practises with Angular

General Web Development

Broader web development topics

Node.js

All about Node.js

React

From Basic concepts to unidirectional data flows

DON'T MISS ANY NEWS